/*
 * Copyright (C) 2010 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.loaders.internal;

import java.util.*;
import java.util.Map.Entry;
import ch.msoftch.internal.*;
import ch.trackedbean.loaders.*;
import ch.trackedbean.loaders.lists.*;
import ch.trackedbean.loaders.objects.*;

/**
 * Default implementation of {@link ILoaderStrategy} providing a cache for list loads and routing object calls directly to the supplied loader.
 * 
 * @author M. Hautle
 */
public class DefaultLoadStrategy implements ILoaderStrategy {
    /** The list cache. */
    // TODO cache with lock -> avoid to trigger a load if one is already running....
    protected final ICache<Key, List> cache = CacheFactory.createCache(Key.class, List.class);

    /** The listeners. */
    protected final DataEventBroadcaster listeners = new DataEventBroadcaster();

    /**
     * {@inheritDoc}
     */
    @Override
    @SuppressWarnings("unchecked")
    public <T, D, K, V> List<T> getList(IListLoader<T, D, K, V> loader, Class<T> type, D subType, Map<K, V> settings, boolean forceLoad) throws Exception {
        final boolean cachable = loader.isCachable(type, subType, settings);
        // avoid key building for uncachable entries...
        final Key k = cachable ? buildKey(type, subType, settings) : null;
        // cache lookup
        if (k != null && !forceLoad) {
            final List res = cache.get(k);
            if (res != null)
                return new ArrayList<T>(res);
        }
        // load
        final List<T> res = loader.load(type, subType, settings);
        // update cache
        if (cachable)
            cache.put(k, new ArrayList<T>(res));
        return res;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public <T, K> T load(IObjectHandler<T, K> handler, Class<T> type, K key, boolean forceLoad) throws Exception {
        return handler.load(type, key);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public <T, K> T persist(IObjectHandler<T, K> handler, T value) throws Exception {
        final T res = handler.persist(value);
        fireObjectCreated(res);
        return res;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public <T, K> T update(IObjectHandler<T, K> handler, T value) throws Exception {
        final T res = handler.update(value);
        fireObjectChanged(res);
        return res;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public <T, K> boolean remove(IObjectHandler<T, K> handler, Class<T> type, K key) throws Exception {
        final boolean res = handler.remove(type, key);
        if (res)
            listeners.fireObjectRemovedEvent(type, key);
        return res;
    }

    /**
     * Fires a event to the concerned {@link IDataListener}s informing them about the creation.<br>
     * Subclasses may override to provide a more accurate handling the just calling {@link IDataListener#complexChange(Class, Object, Map)}.
     * 
     * @param <T> The object type
     * @param obj The object
     */
    protected <T> void fireObjectCreated(final T obj) {
        listeners.fireObjectsChangedEvent(obj.getClass(), null, null);
    }

    /**
     * Fires a event to the concerned {@link IDataListener}s informing them about the change.<br>
     * Subclasses may override to provide a more accurate handling the just calling {@link IDataListener#complexChange(Class, Object, Map)}.
     * 
     * @param <T> The object type
     * @param obj The object
     */
    protected <T> void fireObjectChanged(final T obj) {
        listeners.fireObjectsChangedEvent(obj.getClass(), null, null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public <T, D, K, V> void fireObjectsChangedEvent(Class<T> type, D subType, Map<K, V> settings) {
        listeners.fireObjectsChangedEvent(type, subType, settings);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public <T, K> void fireObjectChangedEvent(Class<T> type, K key) {
        listeners.fireObjectChangedEvent(type, key);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addDataListener(IDataListener l, Class type) {
        listeners.addDataListener(l, type);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void removeDataListener(IDataListener l, Class type) {
        listeners.removeDataListener(l, type);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addStructureDataListener(IDataListener l, Class type) {
        listeners.addStructureDataListener(l, type);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void removeStructureDataListener(IDataListener l, Class type) {
        listeners.removeStructureDataListener(l, type);
    }

    /**
     * Creates a key object for the given parameters.
     * 
     * @param <T> The content type of the list
     * @param <D> The discriminator type
     * @param <K> The key type for the parameter map
     * @param <V> The value type for the parameter map
     * @param type The content type of the list
     * @param subType Additional string specifying the "subtype" to load (may be null)
     * @param settings Additional settings for the load (may be null)
     * @return A corresponding key
     */
    private <T, D, K, V> Key buildKey(Class<T> type, D subType, Map<K, V> settings) {
        if (settings != null)
            return buildFullKey(type, subType, settings);
        if (subType != null)
            return new SubKey<T, D>(type, subType, buildHash(type, subType, null, null));
        return new Key<T>(type, buildHash(type, null, null, null));
    }

    /**
     * Builds a full key - only required if the settings parameter gets passed.
     * 
     * @param <T> The content type of the list
     * @param <D> The discriminator type
     * @param <K> The key type for the parameter map
     * @param <V> The value type for the parameter map
     * @param type The content type of the list
     * @param subType Additional string specifying the "subtype" to load (may be null)
     * @param settings Additional settings for the load
     * @return The key
     */
    @SuppressWarnings("unchecked")
    private <T, D, K, V> Key buildFullKey(Class<T> type, D subType, Map<K, V> settings) {
        final int size = settings.size();
        final K[] keys = (K[]) new Object[size];
        final V[] values = (V[]) new Object[size];
        int cnt = 0;
        for (Entry<K, V> e : settings.entrySet()) {
            int index = Arrays.binarySearch(keys, 0, cnt, e.getKey(), HashComparator.ME);
            if (index < 0)
                index = -index - 1;
            if (index < cnt) {
                System.arraycopy(keys, index, keys, index + 1, cnt - index);
                System.arraycopy(values, index, values, index + 1, cnt - index);
            }
            keys[index] = e.getKey();
            values[index] = e.getValue();
            cnt++;
        }
        return new FullKey<T, D, K, V>(type, subType, keys, values, buildHash(type, subType, keys, values));
    }

    /**
     * Returns the hash value for the given entries.
     * 
     * @param <T> The content type of the list
     * @param <D> The discriminator type
     * @param <K> The key type for the parameter map
     * @param <V> The value type for the parameter map
     * @param type The content type of the list
     * @param subType Additional string specifying the "subtype" to load or null
     * @param keys The keys of the parameter map or null
     * @param values The values of the parameter map or null
     * @return The hash value
     */
    private <D, T, K, V> int buildHash(Class<T> type, D subType, final K[] keys, final V[] values) {
        int result = 31 + Arrays.hashCode(keys);
        result = 31 * result + ((subType == null) ? 0 : subType.hashCode());
        result = 31 * result + type.hashCode();
        result = 31 * result + Arrays.hashCode(values);
        return result;
    }

    /**
     * Key for a type only request.
     * 
     * @param <T> The type
     * @author M. Hautle
     */
    public static class Key<T> {
        /** The element type. */
        final Class<T> type;

        /** The cached hash value of this key. */
        final int hash;

        /**
         * Default constructor.
         * 
         * @param type The list type
         * @param hash The hash code
         */
        public Key(Class<T> type, int hash) {
            this.type = type;
            this.hash = hash;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public final int hashCode() {
            return hash;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (Key.class != obj.getClass())
                return false;
            final Key other = (Key) obj;
            return type.equals(other.type);
        }
    }

    /**
     * Key for a discriminator request.
     * 
     * @param <T> The type
     * @param <D> The discriminator type
     * 
     * @author M. Hautle
     */
    public static class SubKey<T, D> extends Key<T> {
        /** The discriminator. */
        final D subType;

        /**
         * Default constructor.
         * 
         * @param type The list type
         * @param subType The sub type
         * @param hash The hash code
         */
        public SubKey(Class<T> type, D subType, int hash) {
            super(type, hash);
            this.subType = subType;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (SubKey.class != obj.getClass())
                return false;
            final SubKey other = (SubKey) obj;
            return type.equals(other.type) && subType.equals(other.subType);
        }
    }

    /**
     * Key for a full parameterized request.
     * 
     * @param <T> The content type of the list
     * @param <D> The discriminator type
     * @param <K> The key type for the parameter map
     * @param <V> The value type for the parameter map
     * 
     * @author M. Hautle
     */
    public static class FullKey<T, D, K, V> extends SubKey<T, D> {
        /** The setting keys, ordered by their hash code. */
        final K[] keys;

        /** The setting values, each value has the same array index as it's key in {@link #keys}. */
        final V[] values;

        /**
         * Default constructor.
         * 
         * @param type The list type
         * @param subType The sub type
         * @param keys The keys of the settings, ordered by their hash code
         * @param values The setting values, each value has the same array index as it's key
         * @param hash The hash code
         */
        public FullKey(Class<T> type, D subType, K[] keys, V[] values, int hash) {
            super(type, subType, hash);
            this.keys = keys;
            this.values = values;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (SubKey.class != obj.getClass())
                return false;
            final FullKey other = (FullKey) obj;
            if (!type.equals(other.type))
                return false;
            if (subType == null) {
                if (other.subType != null)
                    return false;
            } else if (!subType.equals(other.subType)) {
                return false;
            }
            return Arrays.equals(keys, other.keys) && Arrays.equals(values, other.values);
        }
    }

    /**
     * Comparator comparing the hash values of two objects against each other.
     * 
     * @author M. Hautle
     */
    public static class HashComparator implements Comparator<Object> {
        /** Single instance of this comparator. */
        private static final HashComparator ME = new HashComparator();

        /**
         * {@inheritDoc}
         */
        @Override
        public int compare(Object o1, Object o2) {
            final int h1 = o1.hashCode();
            final int h2 = o2.hashCode();
            if (h1 == h2)
                return 0;
            if (h1 > h2)
                return 1;
            return -1;
        }
    }
}
